%%%-------------------------------------------------------------------
%%% @doc Understands how to install local packages.
%%% @end
%%%-------------------------------------------------------------------
-module(fax_local_install).

%%--------------------------------------------------------------------
%% Include files
%%--------------------------------------------------------------------
-include("faxien.hrl").

%%--------------------------------------------------------------------
%% External exports
%%--------------------------------------------------------------------
-export([
	 install_erts/2,
	 install_application/2,
	 install_release/4,
	 create_script_and_boot/3,
	 create_script_and_boot/2,
	 overwrite_yes_no/3,
	 create_executable_script/5
	]).

-export([
	 create_faxien_executable_script_by_inspection_on_launch/0
	]).

%%====================================================================
%% External functions
%%====================================================================

%%--------------------------------------------------------------------
%% @doc Install an application from a complte local application package. This application must be untarred. 
%% @spec install_application(AppNameOrPath, InstallationPath) -> ok | {error, Reason}
%% @end
%%--------------------------------------------------------------------
install_application(AppNameOrPath, InstallationPath) ->
    case regexp:match(AppNameOrPath, ".*.tar.gz") of
	{match, _, _} ->
	    TmpAppPath = fax_util:stage_package_tarball(AppNameOrPath),
	    ok = install_application(TmpAppPath, InstallationPath),
	    ok = ewl_file:delete_dir(filename:dirname(TmpAppPath));
	_NoMatch ->
	    case fax_validation:is_package_an_app(AppNameOrPath) of
		false -> 
		    {error, badly_formatted_app};
		true  -> 
		    AppInstallationPath     = fax_paths:installed_app_path(InstallationPath),
		    {ok, {AppName, AppVsn}} = fax_paths:package_dir_to_name_and_vsn(AppNameOrPath),
		    InstalledPackageDir     = fax_paths:installed_app_path(InstallationPath, AppName, AppVsn),
		    install_non_release_package(AppNameOrPath, InstalledPackageDir, AppInstallationPath)
	    end
    end.

%%--------------------------------------------------------------------
%% @doc Install an erts package from a local release package. 
%% @spec install_erts(ErtsPackagePath, InstallationPath) -> ok | exit()
%% @end
%%--------------------------------------------------------------------
install_erts(RawErtsPackagePath, InstallationPath) ->
    ?INFO_MSG("raw erts package path ~p~n", [RawErtsPackagePath]),

    case regexp:match(RawErtsPackagePath, ".*.tar.gz") of
	{match, _, _} ->
	    TmpErtsPath = fax_util:stage_package_tarball(RawErtsPackagePath),
	    ok = install_erts(TmpErtsPath, InstallationPath),
	    ok = ewl_file:delete_dir(filename:dirname(TmpErtsPath));
	_NoMatch ->
	    ErtsPackagePath = ensure_correct_erts_dir(RawErtsPackagePath),
	    case fax_validation:is_package_erts(ErtsPackagePath) of
		false -> 
		    {error, badly_formatted_erts_package};
		true  -> 
		    {ok, {_, ErtsVsn}}   = fax_paths:package_dir_to_name_and_vsn(ErtsPackagePath),
		    InstalledPackageDir  = fax_paths:installed_erts_path(InstallationPath, ErtsVsn),
		    ErtsInstallationPath = fax_paths:installed_erts_path(InstallationPath),
		    install_non_release_package(ErtsPackagePath, InstalledPackageDir, ErtsInstallationPath),
		    fax_util:set_all_executable_perms(InstalledPackageDir ++ "/bin")
	    end
    end.

%%--------------------------------------------------------------------
%% @doc Install a release from a complete local release package. If the lib dir does not contain the apps required this function
%% will go out and check repos in its lib directory. 
%% @spec install_release(Repos, ReleasePackagePath, InstallationPath, IsLocalBoot::bool()) -> ok | exit()
%% @end
%%--------------------------------------------------------------------
install_release(Repos, ReleaseNameOrPath, InstallationPath, IsLocalBoot) ->
    case regexp:match(ReleaseNameOrPath, ".*.tar.gz") of
	{match, _, _} ->
	    TmpReleasePath = fax_util:stage_package_tarball(ReleaseNameOrPath),
	    install_release(Repos, TmpReleasePath, InstallationPath, IsLocalBoot),
	    ewl_file:delete_dir(filename:dirname(TmpReleasePath));
	_NoMatch ->
	    case fax_validation:is_package_a_release(ReleaseNameOrPath) of
		false -> {error, badly_formatted_release};
		true  -> install_release2(Repos, ReleaseNameOrPath, InstallationPath, IsLocalBoot)
	    end
    end.


%%--------------------------------------------------------------------
%% @doc Create script and boot files for a release. 
%% <pre>
%% Example:
%%  create_script_and_boot("/usr/local/erlware/releases/my_rel/3.3.4/my_rel.rel", 
%%                         "/usr/local/erlware/lib/", false). 
%%  The RelFilePath points to the .rel file that is to be used while the LibDir function points to the location of the library
%%  applications to be included in the release. 
%% </pre>
%% @spec create_script_and_boot(RelFilePath, LibDir, IsLocalBoot::list()) -> ok | exit()
%% @end
%%--------------------------------------------------------------------
create_script_and_boot(RelFilePath, LibDir, IsLocalBoot) ->
    %% XXX This is path dependent - this needs to be refactored
    {ok, {RelName, RelVsn}}    = fax_paths:package_dir_to_name_and_vsn(filename:dirname(filename:dirname(RelFilePath))),
    {ok, {_ErtsVsn, AppSpecs}} = fax_util:consult_rel_file(RelFilePath),
    LoadedPaths                = code:get_path(),

    lists:foreach(fun(AppSpec) ->
			  AppName       = atom_to_list(element(1, AppSpec)),
			  AppVsn        = element(2, AppSpec),
			  AppNameAndVsn = AppName ++ "-" ++ AppVsn, 
			  AppPath       = lists:flatten([LibDir, "/", AppNameAndVsn, "/ebin"]), 
			  remove_redundant_paths(LoadedPaths, AppName, AppNameAndVsn),
			  ?INFO_MSG("Adding code path: ~p~n", [AppPath]),
			  code:add_pathz(AppPath)
		  end, AppSpecs),

    Opts = 
	case IsLocalBoot of
	    true  -> [local, no_module_tests];
	    false -> [no_module_tests]
	end,
		      
    case systools:make_script(string:substr(RelFilePath, 1, length(RelFilePath) - 4), Opts) of
	ok -> 
	    io:format("~nInstalling Release ~s-~s -> ok~n", [RelName, RelVsn]),
	    ok;
	Error -> 
	    io:format("ERROR - Make script and boot failed ~p~n", [Error]),
	    exit({error, script_and_boot_failed})
    end.
    
%% @equiv create_script_and_boot(RelFilePath, IsLocalBoot)
%% @spec create_script_and_boot(RelFilePath, IsLocalBoot::list()) -> ok | exit()
create_script_and_boot(RelFilePath, IsLocalBoot) ->
    {ok, InstallationPath} = fax_paths:get_installation_path(),
    LibDir                 = fax_paths:installed_app_path(InstallationPath),
    create_script_and_boot(RelFilePath, LibDir, IsLocalBoot).   

%%--------------------------------------------------------------------
%% @doc helper for overwriting previously instlaled applications. 
%% @spec overwrite_yes_no(FunOnYes, FunOnNo, Target) -> void()
%% @end
%%--------------------------------------------------------------------
overwrite_yes_no(FunOnYes, FunOnNo, Target) when is_list(Target) ->
    case ewl_talk:ask([Target, " is already installed. Overwrite [yes|no]"]) of
	Yes when Yes == $y; Yes == $Y; Yes == "yes" ->
	    ewl_file:delete_dir(Target),
	    FunOnYes();
	No when No == $n; No == $N; No == "no" ->
	    FunOnNo();
	Error ->
	    ?INFO_MSG("user entered \"~p\"~n", [Error]),
	    io:format("Please enter \"yes\" or \"no\"~n"),
	    overwrite_yes_no(FunOnYes, FunOnNo, Target)
    end.

%%--------------------------------------------------------------------
%% @doc This is the entry point to creating an excutable script for a particular release 
%% @todo This needs Windows support. 
%% @spec create_executable_script(ReleasePackagePath, InstallationPath, RelName, RelVsn, ErtsVsn) -> ok | exit()
%% @end
%%--------------------------------------------------------------------
create_executable_script(ReleasePackagePath, InstallationPath, RelName, RelVsn, ErtsVsn) ->
    LauncherTemplateFiles = filelib:wildcard(ReleasePackagePath ++ "/cmds/*"),
    lists:foreach(fun(TemplateFile) -> 
			  TemplateName = filename:basename(filename:absname(TemplateFile)),
			  case regexp:match(TemplateFile, ".*\.tmpl") of
			      {match, _, _} ->
				  ExecutableFile = lists:flatten([InstallationPath,
								  "/bin/",
								  string:substr(TemplateName, 1, length(TemplateName) - 5)]),
				  ok = check_for_existing_executable_script(ExecutableFile), 
				  ok = write_executable_file(ExecutableFile, InstallationPath, RelName, RelVsn, ErtsVsn, TemplateName);
			      _Error ->
				  ?INFO_MSG("A non template file ~p found - skipping~n", [TemplateFile])
			  end
		  end, LauncherTemplateFiles). 

%%--------------------------------------------------------------------
%% @doc This is the entry point to creating an excutable script for a particular release 
%% @todo This needs Windows support. 
%% @spec create_executable_script(ReleasePackagePath, InstallationPath, RelName, RelVsn, ErtsVsn) -> ok | exit()
%% @end
%%--------------------------------------------------------------------
create_faxien_executable_script_by_inspection_on_launch() ->
    {ok, InstallationPath} = fax_paths:get_installation_path(),
    {ok, ErtsVsn}          = numbered_vsn_sub_dir(fax_paths:installed_erts_path(InstallationPath)),
    {ok, FaxienVsn}        = numbered_vsn_sub_dir(fax_paths:installed_release_path(InstallationPath, "faxien")),
    ReleasePackagePath     = fax_paths:installed_release_path(InstallationPath, "faxien", FaxienVsn),
    ok                     = create_executable_script(ReleasePackagePath, InstallationPath, "faxien", FaxienVsn, ErtsVsn).


%%====================================================================
%% Internal functions
%%====================================================================
%%--------------------------------------------------------------------
%% @private
%% @doc pull a numbered version directory out of its subdirectory provided there is only one numbered version directory
%% @end
%%--------------------------------------------------------------------
numbered_vsn_sub_dir(BaseDir) ->
    case filelib:wildcard(BaseDir ++ "/*") of
	[VsnPath] ->
	    Vsn = filename:basename(filename:absname(VsnPath)),
	    case regexp:match(Vsn, "[0-9\.]+$") of
		{match, 1, _} -> {ok, Vsn};
		_             -> {error, {"bad single subdir", Vsn, VsnPath}}
	    end;
        List when length(List) > 1 ->
	    {error,{"more than one subdir", List}};
	Error -> 
	    {error,Error}
    end.

%%--------------------------------------------------------------------
%% @private
%% @todo make this whole function much less path aware particularly with respect to installed releases.
%% @doc Compose the steps needed to install a release. This is the only function that should understand pre installed 
%%      package release paths.
%% @end
%%--------------------------------------------------------------------
install_release2(Repos, ReleasePackagePath, InstallationPath, IsLocalBoot) ->
    ?INFO_MSG("install_release(~p, ~p, ~p, ~p)~n", [Repos, ReleasePackagePath, InstallationPath, IsLocalBoot]),
    {ok, {RelName, RelVsn}} = fax_paths:package_dir_to_name_and_vsn(ReleasePackagePath),
    ReleasePath             = fax_paths:installed_release_path(InstallationPath, RelName, RelVsn),
    RelFilePath             = lists:flatten([ReleasePath, "/release/", RelName, ".rel"]),
    case fax_validation:is_package_a_release(ReleasePath) of
	false -> 
	    ok = ewl_file:delete_dir(ReleasePath), %% An old invalid release could still be there, get rid of it
	    ewl_file:mkdir_p(ReleasePath),
	    ReleasePackageRelFilePath = lists:flatten([ReleasePackagePath, "/releases/", RelVsn, "/", RelName, ".rel"]),
	    {ok, {ErtsVsn, AppSpecs}} = fax_util:consult_rel_file(ReleasePackageRelFilePath),
	    ok = install_erts_package_with_path_or_vsn(Repos, ReleasePackagePath ++ "/erts-" ++ ErtsVsn, 
						       ErtsVsn, InstallationPath), 

	    ok = ewl_file:mkdir_p(InstallationPath ++ "/lib"),
	    ok = install_each_app_from_the_appspecs(Repos, AppSpecs, ReleasePackagePath, InstallationPath),
	    %ok = ewl_file:delete_dir(ReleasePackagePath ++ "/lib");

	    ewl_file:mkdir_p(InstallationPath ++ "/bin"),
	    ewl_file:mkdir_p(InstallationPath ++ "/log"),

	    ewl_file:copy_dir(ReleasePackagePath, ReleasePath),
	    ewl_file:copy_dir(ReleasePath ++ "/releases/" ++ RelVsn, ReleasePath ++ "/release"),
	    ok = ewl_file:delete_dir(ReleasePath ++ "/releases"),

	    build_if_build_file(ReleasePath),
	    ok = create_script_and_boot(RelFilePath, IsLocalBoot),
	    ok = create_executable_script(ReleasePath, InstallationPath, RelName, RelVsn, ErtsVsn);
	true -> 
	    io:format("~p is already present~n", [ReleasePath]), 
	    ?INFO_MSG("~p is already present~n", [ReleasePath]), 
	    overwrite_yes_no(fun() -> install_release(Repos, ReleasePackagePath, InstallationPath, IsLocalBoot) end,  
			     fun() -> {error, already_installed} end, 
			     ReleasePath)
    end.
    
install_erts_package_with_path_or_vsn(Repos, ErtsPackage, ErtsVsn, InstallationPath) ->
    case fax_validation:is_package_erts(ErtsPackage) of
	true  -> install_erts(ErtsPackage, InstallationPath);
	false -> fax_install:install_remote_erts(Repos, ErtsVsn, InstallationPath)
    end.

install_each_app_from_the_appspecs(Repos, [AppSpec|T], ReleasePackagePath, InstallationPath) ->
    AppName          = atom_to_list(element(1, AppSpec)),
    AppVsn           = element(2, AppSpec),
    InstalledAppPath = fax_paths:installed_app_path(InstallationPath, AppName, AppVsn),
    case fax_validation:is_package_an_app(InstalledAppPath) of
	false -> 
	    ok = ewl_file:delete_dir(InstalledAppPath),
	    io:format("Installing Application ~s-~s -> ", [AppName, AppVsn]),
	    ?INFO_MSG("Installing Application ~s-~s to ~p~n", [AppName, AppVsn, InstalledAppPath]),
	    AppPackagePath = ReleasePackagePath ++ "/lib/" ++ AppName ++ "-" ++ AppVsn,
	    Res = 
		case fax_validation:is_package_an_app(AppPackagePath) of
		    true -> 
			ok = install_application(AppPackagePath, InstallationPath); 
		    false -> 
			?INFO_MSG("Local release ~p does not contain the required application ~p-~p; checking repos~n",
				  [ReleasePackagePath, AppName, AppVsn]),
			ok = fax_install:install_remote_application(Repos, AppName, AppVsn, InstallationPath)
		end,
	    io:format("~p~n", [Res]),
	    ?INFO_MSG("Installation of ~s resulted in ~p~n", [AppName, Res]),
	    build_if_build_file(InstalledAppPath);
	true -> 
	    ?INFO_MSG("~s-~s already present locally~n", [AppName, AppVsn])
    end,
    
    install_each_app_from_the_appspecs(Repos, T, ReleasePackagePath, InstallationPath);
install_each_app_from_the_appspecs(_Repos, [], _ReleasePackagePath, _InstallationPath) ->
    ok.

%% Remove any additional paths from the path list. 
remove_redundant_paths([LoadedPath|T], AppName, AppNameAndVsn) ->
    case filename:basename(filename:dirname(LoadedPath)) of
	AppNameAndVsn -> 
	    ?INFO_MSG("Removing the path: ~p~n", [LoadedPath]),
	    code:del_path(LoadedPath);
	_ -> 
	    ok
    end,
    remove_redundant_paths(T, AppName, AppNameAndVsn);
remove_redundant_paths([], _, _) ->
    ok.

%%--------------------------------------------------------------------
%% @private
%% @doc Take a versioned directory from a local package and install it making 
%%      sure that you check for a previously installed version
%% @spec install_non_release_package(PackagePath, InstalledPackageDir, PackageInstallationPath) -> ok | exit()
%% @end
%%--------------------------------------------------------------------
install_non_release_package(PackagePath, InstalledPackageDir, PackageInstallationPath) ->
    ?INFO_MSG("install_non_release_package(~p, ~p, ~p)~n", [PackagePath, InstalledPackageDir, PackageInstallationPath]),
    case filelib:is_dir(InstalledPackageDir) of
	false -> 
	    ewl_file:mkdir_p(PackageInstallationPath),
	    ewl_file:copy_dir(PackagePath, InstalledPackageDir),
	    build_if_build_file(InstalledPackageDir);
	true -> 
	    io:format("~p is already present~n", [InstalledPackageDir]), 
	    {error, {directory_already_present, InstalledPackageDir}}
    end.


%%-------------------------------------------------------------------
%% @private
%% @doc if the package contains a executable build script run it. 
%% Note: for now the build file must exist right beneith the package dir. 
%% @spec build_if_build_file(InstalledAppPath) -> ok | exit()
%% @end
%%-------------------------------------------------------------------
build_if_build_file(InstalledPackagePath) ->
    BuildFile = InstalledPackagePath ++ "/" ++ "build",
    case filelib:is_file(BuildFile) of
	true ->
	    {ok, CWD} = file:get_cwd(),
	    ok        = file:set_cwd(InstalledPackagePath),
	    ?INFO_MSG("setting perms on the build script ~p with result ~p~n", 
				  [BuildFile, fax_util:set_executable_perms(BuildFile)]),
	    ?INFO_MSG("running build file ~p with result ~p~n", [BuildFile, os:cmd(BuildFile)]),
	    ok = file:set_cwd(CWD);
	false ->
	    ?INFO_MSG("no build script ~p present - skipping ~n", [BuildFile]),
	    ok
    end.

%%--------------------------------------------------------------------
%% @private
%% @doc Deal with existing executable scripts.
%% @end
%%--------------------------------------------------------------------
check_for_existing_executable_script(ExecutableFile) ->
    case filelib:is_file(ExecutableFile) of
	true ->
	    io:format("replacing existing executable file at: ~s~n", [ExecutableFile]),
	    file:delete(ExecutableFile),
	    ok;
	false  ->
	    ok
    end.

%% 
%%--------------------------------------------------------------------
%% @private
%% @doc Handle different os's when writing the executable file.
%% @todo This needs Windows support. 
%% @end
%%--------------------------------------------------------------------
write_executable_file(ExecutableFile, InstallationPath, RelName, RelVsn, ErtsVsn, TemplateName) ->
    case erlang:system_info(system_architecture) of
	"win32" ->
	    ?INFO_MSG("SysArch was win32~n", []),
	    io:format("Win 32 is not yet supported.~n");
	SysArch ->
	    ?INFO_MSG("SysArch was ~p~n", [SysArch]),
	    Contents = lists:flatten(["#!/bin/sh\n",
                                      "PROG_PATH=`dirname $0`\n",
                                      "$PROG_PATH/faxien_launcher -t ", TemplateName, " -p ", InstallationPath, " -x ", RelName, " -v ", RelVsn, " -e ", 
				      ErtsVsn, " -- $@"]),
	    ?INFO_MSG("executable script contents ~p~n", [Contents]),
	    Res = file:write_file(ExecutableFile, Contents),
	    ?INFO_MSG("Writing executable file to ~s is ~p~n", [ExecutableFile, Res]),
	    fax_util:set_executable_perms(ExecutableFile),
	    Res
    end.




%%--------------------------------------------------------------------
%% @private
%% @doc make sure that if someone passes a package path like /home/martinjlogan/erts instead of the full path like 
%%      /home/martinjlogan/erts/5.5.5 that we return that latter path. At the same time if the path passed is a traditional
%%      erts path like /home/martinjlogan/erts-5.5.5 do nothing. 
%% @spec ensure_correct_erts_dir(RawErtsPackagePath) -> ErtsDir | exit()
%%--------------------------------------------------------------------
ensure_correct_erts_dir(RawErtsPackagePath) ->
    PackageDir = filename:basename(filename:absname(RawErtsPackagePath)),
    ?INFO_MSG("package dir is ~p~n", [PackageDir]),
    case regexp:match(PackageDir, "(^erts-[0-9\.]+$|^[0-9\.]+$)") of
	{match, 1, _} -> 
	    RawErtsPackagePath;
	_ ->
	    case PackageDir of
		[$e,$r,$t,$s] -> just_erts(RawErtsPackagePath);
		Error         -> {error,{bad_erts_directory, Error}}
	    end
    end.

just_erts(RawErtsPackagePath) ->
    case filelib:wildcard(RawErtsPackagePath ++ "/*") of
	[ErtsVsn] ->
	    case regexp:match(ErtsVsn, ".*\/[0-9\.]+\/?$") of
		{match, 1, _} -> ErtsVsn;
		_             -> {error,{bad_erts_directory, ErtsVsn}}
	    end;
        List when length(List) > 1 ->
	    {error,{bad_erts_directory, List}};
	Error -> 
	    {error,{bad_erts_directory, Error}}
    end.
		 
    
